package de.mmis.utilities.consoleWrapper;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.Caret;

/**
 * Shows output of different threads in one {@link JEditorPane}. The output can
 * be filtered by thread and StdOut/StdErr.
 * 
 * @author Martin Nyolt
 * 
 */
class ThreadConsoleEditorPane extends JPanel {

	private static final long serialVersionUID = -3424322863083758409L;

	private final static class Output {
		protected final Thread thread;
		protected final boolean stdErr;
		protected final String s;

		public Output(final Thread thread, final String s, final boolean stdErr) {
			this.thread = thread;
			this.stdErr = stdErr;

			StringBuilder b = new StringBuilder();

			if (stdErr)
				b.append("<span style=\"color:red;\">");

			for (int i = 0; i < s.length(); i++) {
				char c = s.charAt(i);
				switch (c) {
				case '"':
					b.append("&quot;");
					break;
				case '<':
					b.append("&lt;");
					break;
				case '>':
					b.append("&gt;");
					break;
				case '&':
					b.append("&amp;");
					break;
				case '\n':
					if (stdErr)
						b.append("</span>");
					b.append("<br/>\n");
					if (stdErr)
						b.append("<span style=\"color:red;\">");
					break;
				case '\t':
					b.append("&nbsp;&nbsp;&nbsp;&nbsp;");
					break;
				case ' ':
					b.append("&nbsp;");
					break;
				default:
					b.append(c);
				}
			}

			if (stdErr)
				b.append("</span>");

			this.s = b.toString();
		}
	}

	private final class UpdateOutputThread extends Thread {

		public UpdateOutputThread() {
			super(UpdateOutputThread.class.getSimpleName());
			setDaemon(true);
		}

		@Override
		public void run() {
			while (true) {
				synchronized (outputs) {
					try {
						outputs.wait();
					} catch (InterruptedException e) {
						continue;
					}
				}

				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					// ignore
				}

				updateOutput0();
			}
		}
	}

	/** the opening tags of the HTML-Document */
	private static final String OUTPUT_PREPEND = "<html><body><div style=\"font-family: monospace;\">";

	/** the closing tags of the HTML-Document */
	private static final String OUTPUT_APPEND = "</div></body></html>";

	private JCheckBox cb_stdOut;
	private JCheckBox cb_stdErr;
	private JButton button_threads;

	private final static Object threadsMonitor = new Object();
	private HashSet<Thread> selectedThreads = null;
	private HashSet<Thread> allThreads;

	private JEditorPane pane;
	private JScrollPane scroll_pane;

	/** Thread -> Lines */
	private final LinkedList<Output> outputs = new LinkedList<Output>();

	/** the number of string chars to store */
	private int capacity;

	private boolean autoScroll = true;

	/**
	 * @param capacity
	 *            the number of string chars to store
	 */
	public ThreadConsoleEditorPane(int capacity) {
		this.capacity = capacity;
		new UpdateOutputThread().start();
		initGUI();
	}

	private void initGUI() {
		GridBagLayout layout = new GridBagLayout();

		layout.rowWeights = new double[] { 0.0, 1.0 };
		layout.rowHeights = new int[] { 1, 75 };
		layout.columnWeights = new double[] { 0.0, 0.0, 1.0, 0.0 };
		layout.columnWidths = new int[] { 1, 1, 1, 1 };
		setLayout(layout);

		Insets insets = new Insets(5, 5, 5, 5);

		{
			ActionListener listener = new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					updateOutput();
				}
			};

			cb_stdOut = new JCheckBox("StdOut");
			cb_stdOut.setSelected(true);
			cb_stdOut.addActionListener(listener);
			add(cb_stdOut, new GridBagConstraints(0, 0, 1, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
					insets, 0, 0));

			cb_stdErr = new JCheckBox("StdErr");
			cb_stdErr.setSelected(true);
			cb_stdErr.addActionListener(listener);
			add(cb_stdErr, new GridBagConstraints(1, 0, 1, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
					insets, 0, 0));
		}

		{
			button_threads = new JButton("Select Threads");
			button_threads.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					selectThreads();
				}
			});

			add(button_threads, new GridBagConstraints(3, 0, 1, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
					insets, 0, 0));
		}

		{
			pane = new JEditorPane("text/html", OUTPUT_PREPEND + OUTPUT_APPEND);
			pane.setEditable(false);
			scroll_pane = new JScrollPane(pane);
			add(scroll_pane, new GridBagConstraints(0, 1, 4, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets,
					0, 0));
		}
	}

	private void selectThreads() {
		final ThreadGroupSelection sel;
		HashSet<Thread> threads;
		synchronized (threadsMonitor) {
			threads = allThreads;
		}

		HashSet<Thread> selectedThreads;
		synchronized (outputs) {
			selectedThreads = this.selectedThreads;
		}

		sel = new ThreadGroupSelection(threads, selectedThreads);

		JFrame frame = new JFrame() {
			{
				setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
				JScrollPane scroll = new JScrollPane(sel);
				add(scroll);
				pack();
			}
		};

		sel.addTreeSelectionListener(new TreeSelectionListener() {
			@Override
			public void valueChanged(TreeSelectionEvent e) {
				selectedThreadsUpdated(sel);
			}
		});

		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosed(WindowEvent e) {
				selectedThreadsUpdated(sel);
			}
		});

		frame.setVisible(true);
	}

	private void selectedThreadsUpdated(ThreadGroupSelection sel) {
		synchronized (outputs) {
			selectedThreads = sel.getSelectedThreads();
			System.out.println(selectedThreads);
			if (selectedThreads.isEmpty())
				selectedThreads = null;
			updateOutput();
		}
	}

	public void setAutoScroll(boolean autoScroll) {
		this.autoScroll = autoScroll;
	}

	public void setCapacity(int capacity) {
		this.capacity = capacity;
	}

	public void addOutput(final Thread thread, final String s,
			final boolean stdErr) {

		Output output = new Output(thread, s, stdErr);

		synchronized (outputs) {
			outputs.add(output);
		}

		updateOutput();
	}

	public void addOutput(final String s, final boolean stdErr) {
		addOutput(Thread.currentThread(), s, stdErr);
	}

	private void updateOutput() {
		synchronized (outputs) {
			outputs.notifyAll();
		}
	}

	private void updateOutput0() {
		final StringBuilder b = new StringBuilder();
		HashSet<Thread> threads = new HashSet<Thread>();

		int size = 0;

		synchronized (outputs) {
			Iterator<Output> it = outputs.descendingIterator();
			while (it.hasNext()) {
				Output o = it.next();

				size += o.s.length();

				if (size > capacity)
					// lines will be removed below
					break;

				if (o.thread != null)
					threads.add(o.thread);

				if (selectedThreads != null && o.thread != null
						&& !selectedThreads.contains(o.thread))
					// always show stdin
					continue;

				StringBuilder line = new StringBuilder();

				if (o.thread == null)
					// echo of stdin
					line.append("<span style=\"color:blue;\">");
				else {
					if (o.stdErr && !cb_stdErr.isSelected())
						continue;

					if (!o.stdErr && !cb_stdOut.isSelected())
						continue;
				}

				line.append(o.s);

				if (o.thread == null)
					line.append("</span>");

				b.insert(0, line.toString());
			}

			// remove lines to match capacity
			// the check size > capacity is jst to ensure that any lines needs
			// to be removed, size is deliberately not updated
			while (it.hasNext() && size > capacity) {
				it.next();
				it.remove();
			}
		}

		b.insert(0, OUTPUT_PREPEND);
		b.append(OUTPUT_APPEND);
		// final Rectangle rect = pane.getVisibleRect();

		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				Caret caret = pane.getCaret();
				JScrollBar v = scroll_pane.getVerticalScrollBar();

				if (!autoScroll)
					pane.setCaret(null);

				pane.setText(b.toString());

				if (autoScroll) {
					v.setValue(v.getMaximum() - v.getVisibleAmount());
				} else {
					pane.setCaret(caret);
				}
			}
		});

		synchronized (threadsMonitor) {
			this.allThreads = threads;
		}
	}
}
